lucky

lucky

跟着好奇心,去探索我觉得酷的东西 希望以电子报分享所见所思的方式,结识更多朋友,碰撞出更多思维火花
github
bilibili
twitter
medium
youtube
zhihu
mastodon
follow
substack

TypeScript

Official Website#

Basic Types#

Boolean#

The most basic data type is the simple true/false value, known asboolean in JavaScript and TypeScript (the same in other languages).

let isDone: boolean = false;

Number#

Like JavaScript, all numbers in TypeScript are floating-point numbers. The type of these floating-point numbers is number. In addition to supporting decimal and hexadecimal literals, TypeScript also supports binary and octal literals introduced in ECMAScript 2015.

let decLiteral: number = 6;
let hexLiteral: number = 0xf00d;
let binaryLiteral: number = 0b1010;
let octalLiteral: number = 0o744;

String#

Another fundamental operation in JavaScript programs is handling text data from web pages or server-side. Like in other languages, we use string to represent text data types. Like JavaScript, strings can be represented using double quotes (") or single quotes (').

let name: string = "bob";
name = "smith";

You can also usetemplate strings, which can define multi-line text and embedded expressions. These strings are surrounded by backticks (`) and embed expressions in the form of${ expr }.

let name: string = `Gene`;
let age: number = 37;
let sentence: string = `Hello, my name is ${ name }.

I'll be ${ age + 1 } years old next month.`;

This is equivalent to definingsentence as follows:

let sentence: string = "Hello, my name is " + name + ".\n\n" +
    "I'll be " + (age + 1) + " years old next month.";

Array#

TypeScript can manipulate array elements just like JavaScript. There are two ways to define an array. The first way is to follow the element type with [], indicating an array composed of elements of that type:

let list: number[] = [1, 2, 3];

The second way is to use array generics,Array:

let list: Array<number> = [1, 2, 3];

Tuple#

Tuple types allow you to represent an array with a known number and types of elements, where the types of the elements do not have to be the same. For example, you can define a tuple with values of type string and number.

// Declare a tuple type
let x: [string, number];
// Initialize it
x = ['hello', 10]; // OK
// Initialize it incorrectly
x = [10, 'hello']; // Error

When accessing an element at a known index, you will get the correct type:

console.log(x[0].substr(1)); // OK
console.log(x[1].substr(1)); // Error, 'number' does not have 'substr'

When accessing an out-of-bounds element, a union type will be used instead:

x[3] = 'world'; // OK, string can be assigned to (string | number) type

console.log(x[5].toString()); // OK, 'string' and 'number' both have toString

x[6] = true; // Error, boolean is not (string | number) type

Union types are an advanced topic that we will discuss in later chapters.

Enum#

enum types are a supplement to the standard JavaScript data types. Like in other languages such as C#, using enum types allows you to assign friendly names to a set of numeric values.

enum Color {Red, Green, Blue}
let c: Color = Color.Green;

By default, elements are numbered starting from0. You can also manually specify the numeric values of the members. For example, we can modify the above example to start numbering from 1:

enum Color {Red = 1, Green, Blue}
let c: Color = Color.Green;

Alternatively, you can assign all values manually:

enum Color {Red = 1, Green = 2, Blue = 4}
let c: Color = Color.Green;

One convenience provided by enum types is that you can get the name of an enum from its value. For example, if we know the value is 2 but are unsure which name it maps to in Color, we can look up the corresponding name:

enum Color {Red = 1, Green, Blue}
let colorName: string = Color[2];

console.log(colorName);  // Displays 'Green' because its value is 2 in the above code

Any#

Sometimes, we want to specify a type for variables whose type is not clear at the time of programming. These values may come from dynamic content, such as user input or third-party code libraries. In such cases, we do not want the type checker to check these values but rather let them pass through the compile-time checks. We can use the any type to mark these variables:

let notSure: any = 4;
notSure = "maybe a string instead";
notSure = false; // okay, definitely a boolean

The any type is very useful when rewriting existing code, as it allows you to optionally include or remove type checks at compile time. You might think that Object serves a similar purpose, as it does in other languages. However, a variable of type Object only allows you to assign any value to it - but you cannot call any methods on it, even if it really has those methods:

let notSure: any = 4;
notSure.ifItExists(); // okay, ifItExists might exist at runtime
notSure.toFixed(); // okay, toFixed exists (but the compiler doesn't check)

let prettySure: Object = 4;
prettySure.toFixed(); // Error: Property 'toFixed' doesn't exist on type 'Object'.

When you only know part of the data's type, the any type is also useful. For example, you have an array that contains different types of data:

let list: any[] = [1, true, "free"];

list[1] = 100;

Void#

In a way, the void type is the opposite of the any type, indicating the absence of any type. When a function does not return a value, you will typically see its return type as void:

function warnUser(): void {
    console.log("This is my warning message");
}

Declaring a variable of type void is not very useful, as you can only assign it undefined and null:

let unusable: void = undefined;

Null and Undefined#

In TypeScript, undefined and null each have their own types called undefined and null. Similar to void, their intrinsic types are not very useful:

// Not much else we can assign to these variables!
let u: undefined = undefined;
let n: null = null;

By default, null and undefined are subtypes of all types. This means you can assign null and undefined to variables of type number.

However, when you specify the --strictNullChecks flag, null and undefined can only be assigned to void and their respective types. This can help avoid many common issues. Perhaps somewhere you want to pass a string or null or undefined, you can use the union type string | null | undefined. Again, we will introduce union types later.

Note: We encourage using --strictNullChecks whenever possible, but in this handbook, we assume this flag is turned off.

Never#

never type represents the type of values that never occur. For example, the never type is the return type of functions that always throw exceptions or have no return value at all; variables can also be of type never when they are constrained by a type guard that can never be true.

never type is a subtype of every type and can be assigned to any type; however, no type is a subtype of never or can be assigned to never type (except for never itself). Even any cannot be assigned to never.

Here are some functions that return never type:

// Functions that return never must have unreachable end points
function error(message: string): never {
    throw new Error(message);
}

// Inferred return type is never
function fail() {
    return error("Something failed");
}

// Functions that return never must have unreachable end points
function infiniteLoop(): never {
    while (true) {
    }
}

Object#

object represents non-primitive types, which are types other than number, string, boolean, symbol, null, or undefined.

Using the object type allows you to better represent APIs like Object.create. For example:

declare function create(o: object | null): void;

create({ prop: 0 }); // OK
create(null); // OK

create(42); // Error
create("string"); // Error
create(false); // Error
create(undefined); // Error

Type Assertions#

Sometimes you will encounter situations where you know more about a value than TypeScript does. This usually happens when you know that an entity has a more specific type than its existing type.

Through type assertions, you can tell the compiler, "Trust me, I know what I'm doing." Type assertions are like type conversions in other languages, but they do not perform any special data checking or restructuring. They have no runtime impact and only work at compile time. TypeScript assumes that you, the programmer, have performed the necessary checks.

There are two forms of type assertions. One is the "angle-bracket" syntax:

let someValue: any = "this is a string";

let strLength: number = (<string>someValue).length;

The other is the as syntax:

let someValue: any = "this is a string";

let strLength: number = (someValue as string).length;

Interfaces#

Introduction#

One of the core principles of TypeScript is to perform type checking on the structure of values. This is sometimes referred to as "duck typing" or "structural subtyping." In TypeScript, the purpose of interfaces is to name these types and define contracts for your code or third-party code.

Exploring Interfaces#

Let's observe how interfaces work through a simple example:

function printLabel(labelledObj: { label: string }) {
  console.log(labelledObj.label);
}

let myObj = { size: 10, label: "Size 10 Object" };
printLabel(myObj);

The type checker will look at the call to printLabel. printLabel has one parameter and requires that this object parameter has a property named label of type string. It is important to note that the object parameter we passed actually contains many properties, but the compiler only checks whether the required properties exist and whether their types match. However, sometimes TypeScript is not so lenient, which we will explain shortly.

Now let's rewrite the above example using an interface to describe: it must include a label property of type string:

interface LabelledValue {
  label: string;
}

function printLabel(labelledObj: LabelledValue) {
  console.log(labelledObj.label);
}

let myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj);

LabelledValue interface acts like a name to describe the requirements in the above example. It represents an object with a label property of type string. It is important to note that we cannot say that the object passed to printLabel implements this interface like in other languages. We only care about the shape of the value. As long as the passed object meets the necessary conditions mentioned above, it is allowed.

Another point worth mentioning is that the type checker does not check the order of properties; as long as the corresponding properties exist and their types are correct, it is fine.

Optional Properties#

Not all properties in an interface are required. Some may only exist under certain conditions or may not exist at all. Optional properties are commonly used when applying the "option bags" pattern, where only some properties of the parameter object passed to a function are assigned values.

Here is an example of applying "option bags":

interface SquareConfig {
  color?: string;
  width?: number;
}

function createSquare(config: SquareConfig): {color: string; area: number} {
  let newSquare = {color: "white", area: 100};
  if (config.color) {
    newSquare.color = config.color;
  }
  if (config.width) {
    newSquare.area = config.width * config.width;
  }
  return newSquare;
}

let mySquare = createSquare({color: "black"});

Interfaces with optional properties are defined similarly to regular interfaces, just adding a ? symbol after the optional property name.

One benefit of optional properties is that they allow for pre-definition of potentially existing properties, and another benefit is that they can catch errors when referencing non-existent properties. For example, if we intentionally misspell the color property name in createSquare, we will receive an error message:

interface SquareConfig {
  color?: string;
  width?: number;
}

function createSquare(config: SquareConfig): { color: string; area: number } {
  let newSquare = {color: "white", area: 100};
  if (config.clor) {
    // Error: Property 'clor' does not exist on type 'SquareConfig'
    newSquare.color = config.clor;
  }
  if (config.width) {
    newSquare.area = config.width * config.width;
  }
  return newSquare;
}

let mySquare = createSquare({color: "black"});

Readonly Properties#

Some object properties can only be modified when the object is first created. You can specify a readonly property by placing readonly before the property name:

interface Point {
    readonly x: number;
    readonly y: number;
}

You can construct a Point by assigning an object literal. After assignment, x and y can no longer be changed.

let p1: Point = { x: 10, y: 20 };
p1.x = 5; // error!

TypeScript has a ReadonlyArray type, which is similar to Array, but removes all mutable methods, ensuring that the array cannot be modified after creation:

let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
ro[0] = 12; // error!
ro.push(5); // error!
ro.length = 100; // error!
a = ro; // error!

In the last line of the above code, you can see that even assigning an entire ReadonlyArray to a regular array is not allowed. However, you can rewrite it using type assertions:

a = ro as number[];

readonly vs const#

The simplest way to determine whether to use readonly or const is to look at whether it is used as a variable or as a property. If used as a variable, use const, and if as a property, use readonly.

Extra Property Checks#

In the first example, we used an interface, and TypeScript allowed us to pass { size: number; label: string; } to a function that only expected { label: string; }. We have learned about optional properties and know that they are useful in the "option bags" pattern.

However, naively combining these two can lead to issues, just like in JavaScript where you can shoot yourself in the foot. For example, take the createSquare example:

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.